Skip to main content

Silent Session Check

The silent session check lets a site on abair.ie find out whether the visitor is already signed in at auth.abair.ie, and get a session of its own if they are, without navigating anywhere. The site loads auth.abair.ie/session-check in a hidden iframe, and that page answers over postMessage. The same check lets a signed-in site notice that the user has signed out somewhere else, and sign out too.

The ABAIR website uses it on every page that reads the sign-in state through useAuthSession, which today is the synthesis and speech-to-speech pages.


Sites using it​

SitePicks up a sign-inFollows sign-outsWhere
abair.ie, www.abair.ie, preview.abair.ieYes, once per tabYesabair_website: hooks/useAuthSession.ts
leighdom.abair.ieYes, every 10 minutes at mostYesfile-tts-tool: components/auth/SilentSignIn.tsx
mileglornanog.abair.ieYes, once per tab, and again on /loginNomile-glor-na-nog: lib/AuthContext.tsx, app/login/page.tsx

Míle Glór na nÓg doesn't follow sign-outs because its own sign-in is the Embedded Flow, which keeps its session apart from the auth.abair.ie sign-in. Someone who signs in through it is signed out as far as auth.abair.ie can tell, so following sign-outs would sign them out again at once. Its /login page runs the check alongside the embedded form, and moves the visitor on if the check signs them in first.


What the website does​

Visitor on the siteWhen the site checksOutcome
Not signed inOnce per browser tab, on the first page that uses useAuthSessionIf they're signed in at auth.abair.ie, the site redeems a one-time token and is signed in, no redirect. Otherwise nothing changes
Signed inOn page load, and whenever the tab becomes visible again, at most once a minuteIf auth.abair.ie says they've signed out, the site signs out locally. Anything else changes nothing
  • Once per tab. The signed-out check sets abair.ssoChecked in sessionStorage before it runs, so a visitor who isn't signed in doesn't pay for the iframe on every page. A tab that has already checked won't check again: someone who signs in at auth.abair.ie in another tab picks it up in a new tab, or by pressing Sign in, which goes straight through the Redirect Flow without showing the form.
  • Timeout. The site waits up to 8 seconds for an answer, then gives up and removes the iframe. A timeout is treated like unknown: nothing happens.

Sequence​


Message contract​

All three messages carry a type. Anything else, or anything from an unexpected window or origin, is ignored.

1. Frame → site: ready. Sent as soon as the page loads, addressed to "*". It carries no data, so it's safe to send before the frame knows who its parent is.

{ type: "SESSION_CHECK_READY" }

2. Site → frame: request. Addressed to the auth origin only. nonce is a fresh random string; handoff says whether the site wants a sign-in token (it defaults to true, for sites built before the field existed). A site that is already signed in sends handoff: false, so no token is minted for nothing.

{ type: "SESSION_CHECK_REQUEST", nonce: "<random>", handoff: true }

3. Frame → site: response. Addressed to the exact origin that sent the request, never "*". The page answers once per load, so create a new iframe for every check.

{
type: "SESSION_CHECK_RESPONSE",
nonce: "<the nonce you sent>",
status: "signed_in" | "signed_out" | "unknown",
handoff: { token_hash: "…", type: "magiclink" } | null,
}

Status​

statusMeaningWhat a site should do
signed_inThere is a session at auth.abair.ie, and the server confirmed it's still liveIf handoff is present, redeem it with verifyOtp
signed_outThere is definitely no session: never signed in, signed out, or revoked by a sign-out elsewhere. A revoked session is also cleared from auth.abair.ie itselfIf the site is signed in, sign out locally
unknownIt couldn't tell: offline, a network error, or Supabase unreachableNothing. Never sign anyone out on this

A response without a status (from an older version of the page) should be treated as unknown.

handoff can be null even when the status is signed_in: either the site sent handoff: false, or the gateway couldn't issue a token. There is deliberately no fallback to the session tokens here. A silent sign-in that can't give the site its own session leaves it signed out.


Why a one-time token instead of the session​

Copying auth.abair.ie's access_token + refresh_token into another site with setSession makes both sites share one session and one refresh token. Whichever refreshes first invalidates the other's copy, and Supabase's refresh-token reuse detection can then end the session everywhere.

Instead, auth.abair.ie trades the user's access token at the gateway for a magic-link token_hash (Supabase generates it, but no email is sent). The site redeems it with supabase.auth.verifyOtp({ token_hash, type }) and gets a brand-new session of its own, which refreshes independently.

The gateway endpoint (POST https://api.abair.ie/v4/session-handoff) only issues a token when:

  • the call comes from https://auth.abair.ie (the gateway's ALLOWED_ORIGINS);
  • the access token is valid and was issued in the last 120 seconds (SESSION_HANDOFF_MAX_TOKEN_AGE_SECONDS). auth.abair.ie refreshes right before calling, so a stolen access token on its own can't be turned into a session.

The token is single-use: once redeemed, it can't be redeemed again.


The same hand-off in the redirect flow​

The one-time token also works with the normal Redirect Flow. Add handoff=token_hash to the sign-in link:

https://auth.abair.ie/?ref=https://abair.ie/auth/callback&app_name=Abair&handoff=token_hash

After sign-in, the user comes back with the token after the #, which browsers never send to the server, so it stays out of access logs:

https://abair.ie/auth/callback#token_hash=…&type=magiclink

If a token couldn't be issued, sign-in still works: auth.abair.ie falls back to the session tokens in the query string, as described in the Redirect Flow. The website's /auth/callback page handles both. The handoff parameter is carried through detours to sign-up or the profile page.

Sites that can't use the iframe​

A site that isn't on the session-check allowlist can still do a silent check with a full-page round trip, by adding prompt=none to the sign-in link. If the user is signed in, they come straight back with a session as usual. If not, they come straight back with ?error=login_required instead of being shown the sign-in form. Both only happen when ref is a trusted ABAIR address; any other ref gets the normal sign-in page.


Security​

  • Who can receive an answer is enforced twice. The /session-check frame-ancestors CSP stops any other site from embedding the page at all. The page also checks the origin the browser reports on each request (isSessionCheckOrigin) and addresses its reply to that origin only.
  • The allowlist is exact origins, not every *.abair.ie site: https://abair.ie, https://www.abair.ie, https://preview.abair.ie, https://leighdom.abair.ie and https://mileglornanog.abair.ie. Any origin on it can get a session for anyone signed in, without the user doing anything, so only add sites that are actively maintained.
  • The site trusts only its own iframe. It accepts a response only from the iframe it created (event.source), from the auth origin, carrying the nonce it sent.
  • The iframe is sandboxed (allow-scripts allow-same-origin): it can't navigate the site's page, open popups or submit forms.
  • The page runs no third-party scripts. Its CSP allows scripts from auth.abair.ie only, and connections only to Supabase and api.abair.ie. Cloudflare injects an inline bot-detection script into every auth.abair.ie page; on /session-check the browser blocks it, so a CSP error for it in the console is expected and harmless.
  • Only a definite signed_out signs a site out, so an outage or a slow network never signs anyone out.
warning

The check only works for sites on abair.ie. Browsers give an iframe of auth.abair.ie the same storage as auth.abair.ie itself only when the page around it is on the same site (abair.ie). Embedded in a site on any other domain, the iframe gets separate, empty storage and will always answer signed_out.


Shared sessions​

A site that follows sign-outs must not hold a copy of auth.abair.ie's own session. The Redirect Flow without handoff=token_hash hands the site auth.abair.ie's own access_token + refresh_token, so the two share one refresh token.

The check loads auth.abair.ie often, and it refreshes its session when the access token is due. When the site and auth.abair.ie both refresh the same session, the second one presents a refresh token that has already been used. Supabase takes that as a stolen token and ends the session. The check then answers signed_out, and the site signs itself out, typically about an hour after signing in.

So a site that follows sign-outs should ask for the one-time token on its normal sign-in too (handoff=token_hash), as the ABAIR website and Léigh Dom do. A site whose sign-in doesn't sign the visitor in on auth.abair.ie at all, like Míle Glór na nÓg with its embedded form, should only use the check to pick up a sign-in.


Adding a site​

A new site has to be added in three places. If one is missing, the check fails silently: the iframe is blocked or never answers, and the site times out after 8 seconds.

  1. SESSION_CHECK_ORIGINS in the auth repo's src/config/authOrigins.js.
  2. The /session-check frame-ancestors line in the auth repo's public/_headers, with the same origins.
  3. The site's own CSP: frame-src https://auth.abair.ie, plus connect-src for Supabase so verifyOtp can run. For the ABAIR website this lives in the nginx config in infrastructure-automation (networking/nginx-config/abair.ie and www.abair.ie).

Local development​

A development build of auth (npm run dev, on http://localhost:5173 by default) also accepts http://localhost:3000 and http://localhost:3006 as parents. Point the website at it with NEXT_PUBLIC_ABAIR_AUTH_URL=http://localhost:5173.

Locally, the check can report status but can't hand off a session: the hand-off request always goes to https://api.abair.ie, which only accepts calls from https://auth.abair.ie. The production auth.abair.ie doesn't answer localhost either, so test the full sign-in on preview.abair.ie.


Where the code lives​

PieceRepoFile
The session-check pageauthsrc/pages/SessionCheckPage.jsx
Allowed sitesauthsrc/config/authOrigins.js (SESSION_CHECK_ORIGINS), src/utils/redirect.js (isSessionCheckOrigin)
CSP for /session-checkauthpublic/_headers
Requesting a tokenauthsrc/services/sessionHandoff/requestSessionHandoff.js
handoff=token_hash and prompt=noneauthsrc/pages/RedirectAuthPage.jsx
Issuing a tokenapi-gateway-productionroutes/sessionHandoff.js
Asking the iframeabair_websitelib/ssoSessionCheck.ts
Redeeming a tokenabair_websitelib/sessionHandoff.ts
When to checkabair_websitehooks/useAuthSession.ts
Redirect sign-in returnabair_websiteapp/auth/callback/page.tsx